package com.puzzlebazar.client.puzzle.view;

import java.util.Iterator;
import java.util.Vector;

import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.puzzlebazar.client.puzzle.model.History;
import com.puzzlebazar.client.puzzle.model.HistoryMove;
import com.puzzlebazar.client.puzzle.model.HistoryMoveAddMove;
import com.puzzlebazar.client.puzzle.model.HistoryMoveChangeTime;
import com.puzzlebazar.client.puzzle.model.Move;
import com.puzzlebazar.client.puzzle.model.MoveImpl;
import com.puzzlebazar.client.puzzle.model.MoveTimer;
import com.puzzlebazar.client.puzzle.model.PuzzleModel;
import com.puzzlebazar.client.puzzle.view.HistoryNode.HistoryNodeCallback;
import com.puzzlebazar.client.util.FloatConnector;
import com.puzzlebazar.client.util.HorizontalSlider;
import com.puzzlebazar.client.util.Observable;
import com.puzzlebazar.client.util.ObservableImpl;
import com.puzzlebazar.client.util.Observer;
import com.puzzlebazar.client.util.Vec2i;

public class HistoryViewEditable extends HistoryView implements HistoryNodeCallback, ToolbarContainer {

	protected static final int TOOLBAR_HEIGHT = 40;

	protected static final int ZOOMBAR_HEIGHT = 40;

	protected static final int ZOOMLABEL_WIDTH = 45;
	protected static final int PANLABEL_WIDTH = 45;

	
	private static final int NODE_HEIGHT = 16;
	private static final int NODE_WIDTH = 16;
	private static final int NODE_OFFSET_X = BAR_MARGIN_X - NODE_WIDTH/2;
	private static final int NODE_POS_Y = (BAR_CONTAINER_HEIGHT - NODE_HEIGHT)/2;

	// Used to cache the index of the last move that was searched
	private transient int cachedIndex = 0;

	private HorizontalPanel toolbarContainer;
	private HorizontalPanel zoombarContainer;
	private AbsolutePanel nodeContainer;
	private HorizontalSlider zoomSlider;
	private HorizontalSlider panSlider;
	
	
	public HistoryViewEditable(final History history, 
			final MoveTimer moveTimer,
			final PuzzleModel puzzleModel) {
		super(history, moveTimer, puzzleModel);

		toolbarContainer = new HorizontalPanel();
		toolbarContainer.setWidth( "100%" );
		toolbarContainer.setHeight( TOOLBAR_HEIGHT + "px" );
		toolbarContainer.setHorizontalAlignment( HasHorizontalAlignment.ALIGN_CENTER );

		container.add( toolbarContainer, 0, 0 );

		zoombarContainer = new HorizontalPanel();
		zoombarContainer.setWidth( "100%" );
		zoombarContainer.setHeight( ZOOMBAR_HEIGHT + "px" );
		zoombarContainer.setHorizontalAlignment( HasHorizontalAlignment.ALIGN_CENTER );
		zoombarContainer.setVerticalAlignment( HasVerticalAlignment.ALIGN_MIDDLE );
		
		abstract class HistoryViewInfoConnector extends ObservableImpl implements FloatConnector, Observer {

			HistoryViewInfoConnector() {
				historyViewInfo.addObserver( this );
			}
			
			@Override
			public void update(Observable observable, Object arg) {
				setChanged();
				notifyObservers(arg);
			}			
		};
		
		
		FloatConnector zoomConnector = new HistoryViewInfoConnector() {

			@Override
			public float getFloatValue() {
				return historyViewInfo.getZoom();
			}

			@Override
			public void setFloatValue(float value) throws Exception {
				historyViewInfo.setZoom(value);
				setChanged();
				historyViewInfo.notifyObservers();
			}

		};
		
		zoomSlider = new HorizontalSlider( zoomConnector, 0, 5 );

		FloatConnector panConnector = new HistoryViewInfoConnector() {

			@Override
			public float getFloatValue() {
				return historyViewInfo.getPan();
			}

			@Override
			public void setFloatValue(float value) throws Exception {
				historyViewInfo.setPan(value);
				setChanged();
				historyViewInfo.notifyObservers();
			}

		};
		
		panSlider = new HorizontalSlider( panConnector, 0, 1 );

		Label zoomLabel = new Label( "Zoom :" );
		zoomLabel.setWidth( ZOOMLABEL_WIDTH + "px" );
		zoomLabel.setHorizontalAlignment( HasHorizontalAlignment.ALIGN_RIGHT );
		
		Label panLabel = new Label( "Pan :" );
		panLabel.setWidth( PANLABEL_WIDTH + "px" );
		panLabel.setHorizontalAlignment( HasHorizontalAlignment.ALIGN_RIGHT );

		zoombarContainer.add( zoomLabel );
		zoombarContainer.add( zoomSlider );
		zoombarContainer.add( panLabel );
		zoombarContainer.add( panSlider );
		
		container.add( zoombarContainer, 0, TOOLBAR_HEIGHT );
	}
	
	@Override
	protected int getHeaderHeight() {
		return TOOLBAR_HEIGHT + ZOOMBAR_HEIGHT;
	}
	
	@Override
	protected void addNodeContainer() {
		nodeContainer = new AbsolutePanel(); 
		nodeContainer.setHeight( BAR_CONTAINER_HEIGHT + "px" );		
		barContainer.add( nodeContainer, 0, 0 );
	}	
	

	@Override
	public void screenSizeChanged(Vec2i screenSize) {
		super.screenSizeChanged(screenSize);
				
		int nodeContainerWidth = barWidth + 2 * BAR_MARGIN_X;
		nodeContainer.setPixelSize( nodeContainerWidth, BAR_CONTAINER_HEIGHT );
		
		Vec2i zoomSliderSize = new Vec2i( screenSize.x/2 - ZOOMLABEL_WIDTH, screenSize.y );
		zoomSlider.setWidth( zoomSliderSize.x + "px" );
		zoomSlider.screenSizeChanged(zoomSliderSize);
		Vec2i panSliderSize = new Vec2i( screenSize.x/2 - PANLABEL_WIDTH, screenSize.y );		
		panSlider.setWidth( zoomSliderSize.x + "px" );
		panSlider.screenSizeChanged(panSliderSize);
		
		updateNodesPos();
	}

	/**
	 * Called whenever the zoom and pan levels are changed
	 */
	protected void updateZoomAndPan() {
		super.updateZoomAndPan();
		updateHistory(null);
	}
	
	/**
	 * Called whenever history has been updated 
	 * 
	 * @param arg Extra information
	 */
	protected void updateHistory(Object arg) {
	
		if( arg instanceof HistoryMoveAddMove ) {
			HistoryMoveAddMove moveAddMove = (HistoryMoveAddMove)arg;
			if( !moveAddMove.anyMoveDeleted() && moveAddMove.getTime() != MoveImpl.NO_TIME ) 
				addNode( moveAddMove.getPuzzleMove() );
			else 
				recreateAllNodes();
			updateNodesPos();
		}
		else if( !(arg instanceof HistoryMoveChangeTime) ) {
			recreateAllNodes();
			updateNodesPos();
		}
		
		super.updateHistory(arg);
	}	


	
	/**
	 * Adds a single node based on the specified move, does not position it
	 * 
	 * @param move The move corresponding to the node to add
	 */
	private void addNode(Move move) {
		nodeContainer.add( new HistoryNode( move, this ) );
	}

	/**
	 *  Recreate all the nodes, but doesn't position them.
	 */
	private void recreateAllNodes() {

		nodeContainer.clear();
		
		Vector<Move> moves = history.getMoves();		
		for( Move move : moves )
			addNode( move );

	}

	/**
	 * Updates the position of all nodes in the bar. This should
	 * be called after a resize or when the duration changes
	 */
	private void updateNodesPos() {

		Vector<Move> moves = history.getMoves();

		if( moves.size() != nodeContainer.getWidgetCount() ) {
			recreateAllNodes();
			moves = history.getMoves();
		}
		
			
		float timeToPosXMultiplier = computeTimeToPosXMultiplier();
		
		int count = nodeContainer.getWidgetCount();
		for ( int index = 0 ; index < count; ++index ) {
			HistoryNode node = (HistoryNode)nodeContainer.getWidget(index);
			Move move = node.getMove();
			int posX = convertTimeToNodePosX(move.getTime(), timeToPosXMultiplier, computeTimeToPosXOffset(timeToPosXMultiplier) );			
			if( posX < 0 )
				node.setVisible(false);
			else {
				node.setVisible(true);
				nodeContainer.setWidgetPosition( node, posX, NODE_POS_Y );
			}
			
		}
	}
	
	/**
	 * Method to convert a time to a node position
	 * 
	 * @param time  The time to convert
	 * @param timeToPosXMultiplier  The multiplication factor
	 * @param timeToPosXOffset The offset factor
	 * @return The X position corresponding to the passed time
	 */
	private int convertTimeToNodePosX(long time, float timeToPosXMultiplier, long timeToPosXOffset ) {
		int value = (int)( (time - timeToPosXOffset) * timeToPosXMultiplier);
		if( value < 0 || value > barWidth )
			return -1;
		return NODE_OFFSET_X + value;
	}
	
	/**
	 * Method to convert a time to a node position
	 * 
	 * @param nodePosX The node X position, relative to parent
	 * @param posXToTimeMultiplier  The multiplication factor
	 * @param posXToTimeOffset The multiplication offset
	 * @return The time corresponding to the passed node position
	 */
	private long convertNodePosXToTime(int nodePosX, float posXToTimeMultiplier, long posXToTimeOffset ) {
		return (long)(posXToTimeOffset + (nodePosX - NODE_OFFSET_X) * posXToTimeMultiplier + 0.5);
	}
	
	/**
	 * Method to convert a time to a node position
	 * 
	 * @param nodePosX The node X position, relative to parent
	 * @return The time corresponding to the passed node position
	 */
	private long convertNodePosXToTime(int nodePosX) {
		float multiplier = computePosXToTimeMultiplier();
		return convertNodePosXToTime(nodePosX, multiplier, computePosXToTimeOffset(multiplier) );
	}
	
	@Override
	public int clampToValidPosXRetimeNode(HistoryNode node, int posX) {

		int index = getMoveIndex( node.getMove() );
		assert( index >= 0 );
	
		float timeToPosXMultiplier = computeTimeToPosXMultiplier();
		long timeToPosXOffset     = computeTimeToPosXOffset(timeToPosXMultiplier);

		Vector<Move> moves = history.getMoves();

		int minPosX, maxPosX;
		if( index == 0 )
			minPosX = convertTimeToNodePosX( 0, timeToPosXMultiplier, timeToPosXOffset );
		else
			minPosX = convertTimeToNodePosX( moves.get(index-1).getTime(), timeToPosXMultiplier, timeToPosXOffset );
		if( minPosX < 0 )
			minPosX = NODE_OFFSET_X;
		
		if( index == moves.size()-1 )
			maxPosX = convertTimeToNodePosX( history.getDuration(), timeToPosXMultiplier, timeToPosXOffset );
		else
			maxPosX = convertTimeToNodePosX( moves.get(index+1).getTime(), timeToPosXMultiplier, timeToPosXOffset );
		if( maxPosX < 0 )
			maxPosX = barWidth - NODE_OFFSET_X;

		if( posX < minPosX )
			posX = minPosX;
		if( posX > maxPosX )
			posX = maxPosX;
		
		return posX;
	}


	@Override
	public int clampToValidPosXRetimeNodeAndFollowing(HistoryNode node, int posX) {

		int index = getMoveIndex( node.getMove() );
		assert( index >= 0 );
	
		float timeToPosXMultiplier = computeTimeToPosXMultiplier();
		long timeToPosXOffset = computeTimeToPosXOffset(timeToPosXMultiplier);

		Vector<Move> moves = history.getMoves();

		int minPosX, maxPosX;
		if( index == 0 )
			minPosX = convertTimeToNodePosX( 0, timeToPosXMultiplier, timeToPosXOffset );
		else
			minPosX = convertTimeToNodePosX( moves.get(index-1).getTime(), timeToPosXMultiplier, timeToPosXOffset );
		if( minPosX < 0 )
			minPosX = NODE_OFFSET_X;
		
		maxPosX = convertTimeToNodePosX( history.getDuration(), timeToPosXMultiplier, timeToPosXOffset );
		if( maxPosX < 0 )
			maxPosX = barWidth - NODE_OFFSET_X;

		if( posX < minPosX )
			posX = minPosX;
		if( posX > maxPosX )
			posX = maxPosX;
		
		return posX;
	}

	@Override
	public void retimeNode(HistoryNode node, int posX) {
		try {
			history.retimeMove( node.getMove(), convertNodePosXToTime(posX) );
			HistoryMove move = history.mergeOverlappingMoves( puzzleModel );
			history.notifyObservers( move );
			puzzleModel.notifyObservers( move.getPuzzleMove() );
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public void retimeNodeAndFollowing(HistoryNode node, int posX) {
		try {
			history.retimeMoveAndFollowing( node.getMove(), convertNodePosXToTime(posX) );
			HistoryMove move = history.mergeOverlappingMoves( puzzleModel );
			history.notifyObservers( move );
			puzzleModel.notifyObservers( move.getPuzzleMove() );
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void selectNode(HistoryNode node) {
		try {
			HistoryMove move = history.setTime( node.getMove().getTime(), puzzleModel );
			history.notifyObservers( move );
			puzzleModel.notifyObservers( move.getPuzzleMove() );
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Retrieves the index of the passed move into history
	 * 
	 * @param move Move to look for in history
	 * @return The index of this move, -1 if not found
	 */
	private int getMoveIndex(Move move) {
		Vector<Move> moves = history.getMoves();
		try{
			if( moves.get(cachedIndex) == move )
				return cachedIndex;
		}
		catch( ArrayIndexOutOfBoundsException exception ) {}
			
		cachedIndex = history.getMoveIndex( move );
		return cachedIndex;	
	}

	@Override
	public void addToolbar( Toolbar toolbar ) {
		toolbarContainer.add( toolbar );
	}

	@Override
	public void removeToolbar( Toolbar toolbar ) {
		toolbarContainer.remove( toolbar );
	}

	@Override
	public void clearToolbars() {
		toolbarContainer.clear();
	}

	@Override
	public void setAllToolbarVisible( boolean visible ) {
		for (Iterator<Widget> iterator = toolbarContainer.iterator(); iterator.hasNext();) {
			Widget widget = iterator.next();
			if( widget instanceof Toolbar )
				((Toolbar)widget).setVisible(visible);
		}
	}


}
